diff --git a/dumux/freeflow/navierstokes/fluxoveraxisalignedsurface.hh b/dumux/freeflow/navierstokes/fluxoveraxisalignedsurface.hh
new file mode 100644
index 0000000000000000000000000000000000000000..6aeacee13b3d96433b727ff362168064071863bf
--- /dev/null
+++ b/dumux/freeflow/navierstokes/fluxoveraxisalignedsurface.hh
@@ -0,0 +1,381 @@
+// -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+// vi: set et ts=4 sw=4 sts=4:
+/*****************************************************************************
+ *   See the file COPYING for full copying permissions.                      *
+ *                                                                           *
+ *   This program is free software: you can redistribute it and/or modify    *
+ *   it under the terms of the GNU General Public License as published by    *
+ *   the Free Software Foundation, either version 3 of the License, or       *
+ *   (at your option) any later version.                                     *
+ *                                                                           *
+ *   This program is distributed in the hope that it will be useful,         *
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of          *
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the            *
+ *   GNU General Public License for more details.                            *
+ *                                                                           *
+ *   You should have received a copy of the GNU General Public License       *
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.   *
+ *****************************************************************************/
+/*!
+ * \file
+ * \ingroup NavierStokesModel
+ * \copydoc Dumux::FluxOverAxisAlignedSurface
+ */
+#ifndef DUMUX_FREELOW_NAVIERSTOKES_FLUX_OVER_AXISALIGNED_SURFACE_HH
+#define DUMUX_FREELOW_NAVIERSTOKES_FLUX_OVER_AXISALIGNED_SURFACE_HH
+
+#include <algorithm>
+#include <type_traits>
+#include <vector>
+
+#include <dune/common/exceptions.hh>
+#include <dune/geometry/axisalignedcubegeometry.hh>
+
+#include <dumux/common/parameters.hh>
+#include <dumux/geometry/diameter.hh>
+#include <dumux/geometry/distance.hh>
+#include <dumux/geometry/intersectspointgeometry.hh>
+#include <dumux/geometry/geometricentityset.hh>
+#include <dumux/geometry/intersectingentities.hh>
+
+namespace Dumux {
+
+/*!
+ * \ingroup NavierStokesModel
+ * \brief  Class used to calculate fluxes over axis-aligned surfaces.
+ */
+template<class GridVariables, class SolutionVector, class LocalResidual>
+class FluxOverAxisAlignedSurface
+{
+    using Scalar = typename GridVariables::Scalar;
+    using GridGeometry = typename GridVariables::GridGeometry;
+    using FVElementGeometry = typename GridGeometry::LocalView;
+    using SubControlVolumeFace = typename FVElementGeometry::SubControlVolumeFace;
+    using GridView = typename GridGeometry::GridView;
+    using VolumeVariables = typename GridVariables::VolumeVariables;
+    using Element = typename GridView::template Codim<0>::Entity;
+    using NumEqVector = typename LocalResidual::ElementResidualVector::value_type;
+
+    static constexpr auto dim = GridView::dimension;
+    static constexpr auto dimWorld = GridView::dimensionworld;
+
+    static_assert(dim > 1);
+
+    using GlobalPosition = typename Element::Geometry::GlobalCoordinate;
+
+    // in 2D, the surface is represented as a line
+    using SurfaceT = Dune::AxisAlignedCubeGeometry<Scalar, (dim == 2 ? 1 : 2), dimWorld>;
+
+    struct SurfaceData
+    {
+        SurfaceT surface;
+        std::size_t normalDirectionIndex;
+        NumEqVector flux;
+    };
+
+public:
+
+    using Surface = SurfaceT;
+
+    /*!
+     * \brief The constructor
+     */
+    FluxOverAxisAlignedSurface(const GridVariables& gridVariables,
+                               const SolutionVector& sol,
+                               const LocalResidual& localResidual)
+    : gridVariables_(gridVariables)
+    , sol_(sol)
+    , localResidual_(localResidual)
+    {
+        verbose_ = getParamFromGroup<bool>(problem_().paramGroup(), "FluxOverAxisAlignedSurface.Verbose", false);
+    }
+
+    /*!
+     * \brief Add an axis-aligned surface with a given name
+     *
+     * \param name The name of the surface
+     * \param surface The surface to add
+     */
+    template<class T>
+    void addAxisAlignedSurface(const std::string& name, T&& surface)
+    {
+        static_assert(std::is_same_v<std::decay_t<T>, Surface>);
+        surfaces_.emplace(std::make_pair(name, std::make_pair(surface, NumEqVector{})));
+    }
+
+    /*!
+     * \brief Add an axis-aligned surface (segment in 2D) with a given name, specifying the surface's corner points.
+     *
+     * \param name The name of the surface
+     * \param lowerLeft Lower left corner of surface
+     * \param upperRight Upper right corner of surface
+     */
+     void addAxisAlignedSurface(const std::string& name,
+                                const GlobalPosition& lowerLeft,
+                                const GlobalPosition& upperRight)
+    {
+        using std::abs;
+        const GlobalPosition v = upperRight - lowerLeft;
+        const auto it = std::find_if(v.begin(), v.end(), [](const auto& x){ return abs(x) < 1e-20; });
+        if (it == v.end())
+            DUNE_THROW(Dune::InvalidStateException, "Surface is not axis-parallel!");
+
+        const std::size_t normalDirectionIndex = std::distance(v.begin(), it);
+        auto inSurfaceAxes = std::move(std::bitset<dimWorld>{}.set());
+        inSurfaceAxes.set(normalDirectionIndex, false);
+        auto surface = Surface(lowerLeft, upperRight, inSurfaceAxes);
+
+        surfaces_.emplace(std::make_pair(name, SurfaceData{std::move(surface), normalDirectionIndex, NumEqVector{}}));
+    }
+
+    /*!
+     * \brief Add an axis-aligned plane (line in 2D) with a given name, specifying the planes's center and normal.
+     *
+     * \param name The name of the plane
+     * \param center Center point of the plane
+     * \param normalDirectionIndex Index of the plane's normal axis (0=x, 1=y, 2=z)
+     */
+     void addAxisAlignedPlane(const std::string& name,
+                              const GlobalPosition& center,
+                              const std::size_t normalDirectionIndex)
+    {
+        GlobalPosition lowerLeft = gridVariables_.gridGeometry().bBoxMin();
+        GlobalPosition upperRight = gridVariables_.gridGeometry().bBoxMax();
+
+        lowerLeft[normalDirectionIndex] = center[normalDirectionIndex];
+        upperRight[normalDirectionIndex] = center[normalDirectionIndex];
+
+        auto inSurfaceAxes = std::move(std::bitset<dimWorld>{}.set());
+        inSurfaceAxes.set(normalDirectionIndex, false);
+        auto surface = Surface(lowerLeft, upperRight, inSurfaceAxes);
+
+        surfaces_.emplace(std::make_pair(name, SurfaceData{std::move(surface), normalDirectionIndex, NumEqVector{}}));
+    }
+
+    /*!
+     * \brief Calculate the fluxes over all surfaces.
+     */
+    void calculateAllFluxes()
+    {
+        auto fluxType = [this](const auto& element,
+                               const auto& fvGeometry,
+                               const auto& elemVolVars,
+                               const auto& scvf,
+                               const auto& elemFluxVarsCache)
+        {
+            return localResidual_.evalFlux(problem_(), element, fvGeometry, elemVolVars, elemFluxVarsCache, scvf);
+        };
+
+        calculateFluxes(fluxType);
+    }
+
+    /*!
+     * \brief Calculate the fluxes over all surfaces for a given flux type.
+     *
+     * \param fluxType The flux type. This can be a lambda of the following form:
+     *                 [](const auto& element,
+                          const auto& fvGeometry,
+                          const auto& elemVolVars,
+                          const auto& scvf,
+                          const auto& elemFluxVarsCache)
+                          { return ... ; }
+     */
+    template<class FluxType>
+    void calculateFluxes(const FluxType& fluxType)
+    {
+        // make sure to reset all the values of the surfaces, in case this method has been called already before
+        for (auto& surface : surfaces_)
+            surface.second.flux = 0.0;
+
+        snapSurfaceToClosestFace_();
+        calculateFluxes_(fluxType);
+    }
+
+    /*!
+     * \brief Return the flux over given surface
+     *
+     * \param name The name of the surface
+     */
+    const auto& flux(const std::string& name) const
+    {
+        return surfaces_.at(name).flux;
+    }
+
+    /*!
+     * \brief Provides access to all surfaces.
+     */
+    const std::map<std::string, SurfaceData>& surfaces() const
+    { return surfaces_; }
+
+    /*!
+     * \brief Prints all fluxes.
+     */
+    void printAllFluxes() const
+    {
+        for (const auto& [name, data] : surfaces_)
+            std::cout << "Flux over surface " << name << ": " << data.flux << std::endl;
+    }
+
+private:
+
+    template<class FluxType>
+    void calculateFluxes_(const FluxType& fluxType)
+    {
+        auto fvGeometry = localView(problem_().gridGeometry());
+        auto elemVolVars = localView(gridVariables_.curGridVolVars());
+        auto elemFluxVarsCache = localView(gridVariables_.gridFluxVarsCache());
+
+        for (const auto& element : elements(problem_().gridGeometry().gridView()))
+        {
+            fvGeometry.bindElement(element);
+            elemVolVars.bindElement(element, fvGeometry, sol_);
+            elemFluxVarsCache.bindElement(element, fvGeometry, elemVolVars);
+
+            for (const auto& scvf : scvfs(fvGeometry))
+            {
+                // iterate through all surfaces and check if the flux at the given position
+                // should be accounted for in the respective surface
+                for (auto& [name, surfaceData] : surfaces_)
+                {
+                    if (considerScvf_(scvf, surfaceData))
+                    {
+                        const auto result = fluxType(element, fvGeometry, elemVolVars, scvf, elemFluxVarsCache);
+                        surfaceData.flux += result;
+
+                        if (verbose_)
+                            std::cout << "At element " << problem_().gridGeometry().elementMapper().index(element)
+                                      << ": Flux at face " << scvf.ipGlobal() << ": " << result << " (" << name << ")" << std::endl;
+                    }
+                }
+            }
+        }
+    }
+
+    //! Check whether a scvf should be considered for the flux calculation
+    bool considerScvf_(const SubControlVolumeFace& scvf, const SurfaceData& SurfaceData) const
+    {
+        // In order to avoid considering scvfs at the same element intersection (and hence, the corresponding flux) twice,
+        // only use those with a unit outer normal pointing towards positive coordinate direction,
+        // unless the scvf lies on a boundary (then there is no second scvf).
+        if (scvf.boundary() || !std::signbit(scvf.unitOuterNormal()[SurfaceData.normalDirectionIndex]))
+            return intersectsPointGeometry(scvf.ipGlobal(), SurfaceData.surface);
+        else
+            return false;
+    }
+
+    void snapSurfaceToClosestFace_()
+    {
+        using GeometriesEntitySet = Dumux::GeometriesEntitySet<Surface>;
+        const auto gridView = problem_().gridGeometry().gridView();
+
+        for (auto& [name, surfaceData] : surfaces_)
+        {
+            GeometriesEntitySet entitySet({surfaceData.surface});
+            Dumux::BoundingBoxTree<GeometriesEntitySet> geometriesTree(std::make_shared<GeometriesEntitySet>(entitySet));
+            const auto intersectingElements = intersectingEntities(problem_().gridGeometry().boundingBoxTree(), geometriesTree);
+
+            if (intersectingElements.empty())
+            {
+                std::cout << "surface boundaries: " << std::endl;
+                printSurfaceBoundaries_(surfaceData.surface);
+
+                DUNE_THROW(Dune::InvalidStateException, "surface " << name << " does not intersect with any element");
+            }
+
+            std::vector<std::size_t> sortedResults;
+            sortedResults.reserve(gridView.size(0));
+
+            for (const auto& i : intersectingElements)
+                sortedResults.push_back(i.first());
+
+            std::sort(sortedResults.begin(), sortedResults.end());
+            sortedResults.erase(std::unique(sortedResults.begin(), sortedResults.end()), sortedResults.end());
+
+            // pick the first intersecting element and make sure the surface snaps to the closest face with the same (or opposite facing) normal vector
+            GlobalPosition normalVector(0.0);
+            normalVector[surfaceData.normalDirectionIndex] = 1.0;
+
+            const auto& firstIntersectingElement = problem_().gridGeometry().element(sortedResults[0]);
+            Scalar distance = std::numeric_limits<Scalar>::max();
+            bool snappingOcurred = false;
+
+            GlobalPosition surfaceLowerLeft = surfaceData.surface.corner(0);
+            GlobalPosition surfaceUpperRight = surfaceData.surface.corner(3);
+
+            bool surfaceAlreadyOnFaces = false;
+            for (const auto& intersection : intersections(gridView, firstIntersectingElement))
+            {
+                if (surfaceAlreadyOnFaces)
+                    continue;
+
+                using std::abs;
+                if (abs(1.0 - abs(normalVector * intersection.centerUnitOuterNormal())) < 1e-8)
+                {
+
+                    const auto getDistance = [](const auto& p, const auto& geo)
+                    {
+                        if constexpr (dim == 2)
+                            return distancePointSegment(p, geo);
+                        else
+                            return distancePointPolygon(p, geo);
+                    };
+
+                    const auto& geo = intersection.geometry();
+                    if (const Scalar d = getDistance(geo.center(), surfaceData.surface); d < 1e-8 * diameter(geo))
+                    {
+                        // no snapping required, face already lies on surface
+                        surfaceAlreadyOnFaces = true;
+                        snappingOcurred = false;
+                    }
+                    else if (d < distance)
+                    {
+                        distance = d;
+                        snappingOcurred = true;
+
+                        // move the surface boundaries
+                        for (int i = 0; i < surfaceData.surface.corners(); ++i)
+                        {
+                            const auto& faceCenter = geo.center();
+                            surfaceLowerLeft[surfaceData.normalDirectionIndex] = faceCenter[surfaceData.normalDirectionIndex];
+                            surfaceUpperRight[surfaceData.normalDirectionIndex] = faceCenter[surfaceData.normalDirectionIndex];
+                        }
+                    }
+                }
+            }
+
+            if (snappingOcurred)
+            {
+                std::cout << "\n\nSurface '" << name <<  "' was automatically snapped to the closest faces" << std::endl;
+                std::cout << "Old surface boundaries: " << std::endl;
+                printSurfaceBoundaries_(surfaceData.surface);
+
+                // overwrite the old surface with the new boundaries
+                auto inSurfaceAxes = std::move(std::bitset<dimWorld>{}.set());
+                inSurfaceAxes.set(surfaceData.normalDirectionIndex, false);
+                surfaceData.surface = Surface{surfaceLowerLeft, surfaceUpperRight, inSurfaceAxes};
+
+                std::cout << "New surface boundaries: " << std::endl;
+                printSurfaceBoundaries_(surfaceData.surface);
+                std::cout << std::endl;
+            }
+        }
+    }
+
+    void printSurfaceBoundaries_(const Surface& surface) const
+    {
+        for (int i = 0; i < surface.corners(); ++i)
+            std::cout << surface.corner(i) << std::endl;
+    }
+
+    const auto& problem_() const { return gridVariables_.curGridVolVars().problem(); }
+
+    std::map<std::string, SurfaceData> surfaces_;
+    const GridVariables& gridVariables_;
+    const SolutionVector& sol_;
+    const LocalResidual localResidual_; // store a copy of the local residual
+    bool verbose_;
+};
+
+} //end namespace Dumux
+
+#endif
\ No newline at end of file