diff --git a/dumux/discretization/CMakeLists.txt b/dumux/discretization/CMakeLists.txt
index 3a2711c6a933fcb1850aded35424a9f073327903..812b30de3f33e870c69381002771eca417e2ab0b 100644
--- a/dumux/discretization/CMakeLists.txt
+++ b/dumux/discretization/CMakeLists.txt
@@ -2,6 +2,7 @@ add_subdirectory(box)
 add_subdirectory(cellcentered)
 add_subdirectory(fem)
 add_subdirectory(staggered)
+add_subdirectory(projection)
 
 install(FILES
 basefvgridgeometry.hh
diff --git a/dumux/discretization/projection/CMakeLists.txt b/dumux/discretization/projection/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f3f288c353f6a972f27fa5cb6c41534674bb9ff0
--- /dev/null
+++ b/dumux/discretization/projection/CMakeLists.txt
@@ -0,0 +1,3 @@
+install(FILES
+projector.hh
+DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/dumux/discretization/projection)
diff --git a/dumux/discretization/projection/projector.hh b/dumux/discretization/projection/projector.hh
new file mode 100644
index 0000000000000000000000000000000000000000..6124204b469d2d3774b82a58d4acf34d42d697f0
--- /dev/null
+++ b/dumux/discretization/projection/projector.hh
@@ -0,0 +1,337 @@
+// -*- 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 Discretization
+ * \brief Contains functionality for L2-projections from one function
+ *        space into another, which can live both on the same or different
+ *        grids of potentially different dimensionality.
+ * \note In the case of the function space bases living on the same grid
+ *       an optimized implementation could be implemented. Currently, we
+ *       require a glue object containing the intersections between two
+ *       grids to be provided to the free factory functions.
+ */
+#ifndef DUMUX_DISCRETIZATION_PROJECTOR_HH
+#define DUMUX_DISCRETIZATION_PROJECTOR_HH
+
+#include <string>
+#include <utility>
+#include <type_traits>
+
+#include <dune/common/timer.hh>
+#include <dune/common/fmatrix.hh>
+#include <dune/common/exceptions.hh>
+#include <dune/common/promotiontraits.hh>
+
+#include <dune/geometry/quadraturerules.hh>
+#include <dune/istl/matrixindexset.hh>
+
+#include <dumux/common/parameters.hh>
+#include <dumux/linear/seqsolverbackend.hh>
+#include <dumux/assembly/jacobianpattern.hh>
+#include <dumux/discretization/functionspacebasis.hh>
+
+namespace Dumux {
+
+/*!
+ * \ingroup Discretization
+ * \brief Does an L2-projection from one discrete function space
+ *        into another. The convenience functions makeProjectorPair
+ *        or makeProjector can be used to create such a projection.
+ */
+template<class Scalar>
+class Projector
+{
+    using BlockType = Dune::FieldMatrix<Scalar, 1, 1>;
+
+public:
+    //! Export the type of the projection matrices
+    using Matrix = Dune::BCRSMatrix< BlockType >;
+
+    //! delete default constructor
+    Projector() = delete;
+
+    //! Constructor
+    Projector(Matrix&& M, Matrix&& P)
+    : M_(std::move(M)) , P_(std::move(P))
+    {}
+
+    /*!
+     * \brief Project a solution u into up
+     * \param u The solution living on the domain space
+     * \param up The projection of u into the target space
+     */
+    template< class DomainSolution, class TargetSolution>
+    void project(const DomainSolution& u, TargetSolution& up) const
+    {
+        // be picky about size of u
+        if ( u.size() != P_.M())
+            DUNE_THROW(Dune::InvalidStateException, "Vector size mismatch" );
+
+        up.resize(M_.N());
+
+        auto rhs = up;
+        P_.mv(u, rhs);
+
+        SSORCGBackend solver;
+        solver.solve(M_, up, rhs);
+    }
+
+private:
+    Matrix M_;
+    Matrix P_;
+};
+
+/*!
+ * \brief Traits class stating the type of projector between to bases
+ */
+template<class FEBasisDomain, class FEBasisTarget>
+class ProjectorTraits
+{
+    using FiniteElementDomain = typename FEBasisDomain::LocalView::Tree::FiniteElement;
+    using FiniteElementTarget = typename FEBasisTarget::LocalView::Tree::FiniteElement;
+    using ScalarDomain = typename FiniteElementDomain::Traits::LocalBasisType::Traits::RangeFieldType;
+    using ScalarTarget = typename FiniteElementTarget::Traits::LocalBasisType::Traits::RangeFieldType;
+    using Scalar = typename Dune::PromotionTraits<ScalarDomain, ScalarTarget>::PromotedType;
+public:
+    using Projector = Dumux::Projector< Scalar >;
+};
+
+
+// Projector construction details
+namespace Impl {
+
+/*!
+ * \brief Creates a projector class between two function space bases
+ * \tparam doBidirectional If false, the backward projection matrix is not assembled
+ */
+template<bool doBidirectional, class FEBasisDomain, class FEBasisTarget, class GlueType>
+std::pair< typename ProjectorTraits<FEBasisDomain, FEBasisTarget>::Projector,
+           typename ProjectorTraits<FEBasisTarget, FEBasisDomain>::Projector >
+makeProjectorPair(const FEBasisDomain& feBasisDomain,
+                  const FEBasisTarget& feBasisTarget,
+                  const GlueType& glue)
+{
+    // we assume that target dim <= domain dimension
+    static constexpr int domainDim = FEBasisDomain::GridView::dimension;
+    static constexpr int targetDim = FEBasisTarget::GridView::dimension;
+    static_assert(targetDim <= domainDim, "This expects target dim < domain dim, please swap arguments");
+
+    using ForwardProjector = typename ProjectorTraits<FEBasisDomain, FEBasisTarget>::Projector;
+    using BackwardProjector = typename ProjectorTraits<FEBasisTarget, FEBasisDomain>::Projector;
+
+    using ForwardProjectionMatrix = typename ForwardProjector::Matrix;
+    using BackwardProjectionMatrix = typename BackwardProjector::Matrix;
+
+    auto domainLocalView = feBasisDomain.localView();
+    auto targetLocalView = feBasisTarget.localView();
+
+    // determine mass matrix patterns (standard FE scheme pattern)
+    Dune::MatrixIndexSet backwardPatternM, forwardPatternM;
+    forwardPatternM = getFEJacobianPattern(feBasisTarget);
+    if (doBidirectional) backwardPatternM = getFEJacobianPattern(feBasisDomain);
+
+    // determine projection matrix patterns
+    Dune::MatrixIndexSet backwardPatternP, forwardPatternP;
+    forwardPatternP.resize(feBasisTarget.size(), feBasisDomain.size());
+    if (doBidirectional) backwardPatternP.resize(feBasisDomain.size(), feBasisTarget.size());
+
+    using std::max;
+    unsigned int maxBasisOrder = 0;
+    for (const auto& is : intersections(glue))
+    {
+        // "outside" element is of target domain
+        // since target dim <= domain dim there is maximum one!
+        targetLocalView.bind( is.outside(0) );
+        const auto& targetLocalBasis = targetLocalView.tree().finiteElement().localBasis();
+
+        for (unsigned int nIdx = 0; nIdx < is.neighbor(/*targetSide*/1); ++nIdx)
+        {
+            domainLocalView.bind( is.inside(nIdx) );
+            const auto& domainLocalBasis = domainLocalView.tree().finiteElement().localBasis();
+
+            // keep track of maximum basis order (used in integration)
+            maxBasisOrder = max(maxBasisOrder, max(domainLocalBasis.order(), targetLocalBasis.order()));
+
+            for (unsigned int i = 0; i < domainLocalBasis.size(); ++i)
+                for (unsigned int j = 0; j < targetLocalBasis.size(); ++j)
+                {
+                    forwardPatternP.add(targetLocalView.index(j), domainLocalView.index(i));
+                    if (doBidirectional) backwardPatternP.add(domainLocalView.index(i), targetLocalView.index(j));
+                }
+        }
+    }
+
+    // assemble matrices
+    ForwardProjectionMatrix forwardM, forwardP;
+    forwardPatternM.exportIdx(forwardM); forwardM = 0.0;
+    forwardPatternP.exportIdx(forwardP); forwardP = 0.0;
+
+    BackwardProjectionMatrix backwardM, backwardP;
+    if (doBidirectional)
+    {
+        backwardPatternM.exportIdx(backwardM); backwardM = 0.0;
+        backwardPatternP.exportIdx(backwardP); backwardP = 0.0;
+    }
+
+    for (const auto& is : intersections(glue))
+    {
+        const auto& targetElement = is.outside(0);
+        const auto& targetElementGeometry = targetElement.geometry();
+
+        targetLocalView.bind( targetElement );
+        const auto& targetLocalBasis = targetLocalView.tree().finiteElement().localBasis();
+
+        // perform integration over intersection geometry
+        using IsGeometry = typename std::decay_t<decltype(is.geometry())>;
+        using ctype = typename IsGeometry::ctype;
+
+        const auto& isGeometry = is.geometry();
+        const int intOrder = maxBasisOrder + 1;
+        const auto& quad = Dune::QuadratureRules<ctype, IsGeometry::mydimension>::rule(isGeometry.type(), intOrder);
+        for (auto&& qp : quad)
+        {
+            const auto weight = qp.weight();
+            const auto ie = isGeometry.integrationElement(qp.position());
+            const auto globalPos = isGeometry.global(qp.position());
+
+            std::vector< Dune::FieldVector<ctype, 1> > targetShapeVals;
+            targetLocalBasis.evaluateFunction(targetElementGeometry.local(globalPos), targetShapeVals);
+
+            // mass matrix entries target domain
+            for (unsigned int i = 0; i < targetLocalBasis.size(); ++i)
+            {
+                const auto dofIdx = targetLocalView.index(i);
+                forwardM[dofIdx][dofIdx] += ie*weight*targetShapeVals[i];
+            }
+
+            // If targetDim < domainDim, there can be several "neighbors" if
+            // targetElement is aligned with a facet of domainElement. In
+            // this case make sure the basis functions are not added
+            // multiple times! (division by numNeighbors)
+            const auto numNeighbors = is.neighbor(/*targetSide*/1);
+            for (unsigned int nIdx = 0; nIdx < numNeighbors; ++nIdx)
+            {
+                const auto& domainElement = is.inside(nIdx);
+                domainLocalView.bind( domainElement );
+                const auto& domainLocalBasis = domainLocalView.tree().finiteElement().localBasis();
+
+                std::vector< Dune::FieldVector<ctype, 1> > domainShapeVals;
+                domainLocalBasis.evaluateFunction(domainElement.geometry().local(globalPos), domainShapeVals);
+
+                // add entries in matrices
+                for (unsigned int i = 0; i < domainLocalBasis.size(); ++i)
+                {
+                    const auto dofIdxDomain = domainLocalView.index(i);
+                    const auto domainShapeVal = domainShapeVals[i];
+                    backwardM[dofIdxDomain][dofIdxDomain] += ie*weight*domainShapeVal;
+
+                    for (unsigned int j = 0; j < targetLocalBasis.size(); ++j)
+                    {
+                        const auto dofIdxTarget = targetLocalView.index(j);
+                        const auto entry = ie*weight*domainShapeVal*targetShapeVals[j];
+
+                        forwardP[dofIdxTarget][dofIdxDomain] += entry/numNeighbors;
+                        if (doBidirectional)
+                            backwardP[dofIdxDomain][dofIdxTarget] += entry;
+                    }
+                }
+            }
+        }
+    }
+
+    // On those dofs that to not take part in any intersection we will have zeroes
+    // in the mass matrices. The right hand side will be zero because the projection
+    // matrix has no entries for those dofs as there was no intersection. Thus, we set
+    // 1.0 on the diagonal for those dofs, such that after projection, the projected
+    // solution is 0 on them.
+    for (std::size_t dofIdx = 0; dofIdx < forwardM.N(); ++dofIdx)
+        if (forwardM[dofIdx][dofIdx] == 0.0)
+            forwardM[dofIdx][dofIdx] = 1.0;
+
+    if (doBidirectional)
+    {
+        for (std::size_t dofIdx = 0; dofIdx < backwardM.N(); ++dofIdx)
+            if (backwardM[dofIdx][dofIdx] == 0.0)
+                backwardM[dofIdx][dofIdx] = 1.0;
+    }
+
+    ForwardProjector fw(std::move(forwardM), std::move(forwardP));
+    BackwardProjector bw(std::move(backwardM), std::move(backwardP));
+
+    return std::make_pair(std::move(fw), std::move(bw));
+}
+
+} // end namespace Implementation
+
+
+/*!
+ * \ingroup Discretization
+ * \brief Creates a pair of projectors between the space with
+ *        basis feBasisDomain to the space with basis feBasisTarget.
+ * \param feBasisDomain The domain finite element space basis
+ * \param feBasisTarget The target finite element space basis
+ * \param glue The glue object containing the intersections between the grids.
+ * \return A pair of projectors where the first is the forward
+ *         projector from the space with basis feBasisDomain to
+ *         the space with basis feBasisTarget and the second
+ *         does the backward projection.
+ */
+template< class FEBasisDomain, class FEBasisTarget, class GlueType >
+std::pair< typename ProjectorTraits<FEBasisDomain, FEBasisTarget>::Projector,
+           typename ProjectorTraits<FEBasisTarget, FEBasisDomain>::Projector >
+makeProjectorPair(const FEBasisDomain& feBasisDomain,
+                  const FEBasisTarget& feBasisTarget,
+                  GlueType glue)
+{
+    // we assume that target dim <= domain dimension
+    static constexpr int domainDim = FEBasisDomain::GridView::dimension;
+    static constexpr int targetDim = FEBasisTarget::GridView::dimension;
+    static_assert(targetDim <= domainDim, "makeProjectorPair() expects targetDim < domainDim, please swap arguments");
+
+    return Impl::makeProjectorPair<true>(feBasisDomain, feBasisTarget, glue);
+}
+
+/*!
+ * \ingroup Discretization
+ * \brief Creates a forward projector from the space feBasisDomain
+ *        to the space with basis feBasisTarget.
+ * \param feBasisDomain The domain finite element space basis
+ * \param feBasisTarget The target finite element space basis
+ * \param glue The glue object containing the intersections between the grids.
+ * \return The forward projector from the space with basis feBasisDomain
+ *         to the space with basis feBasisTarget.
+ */
+template< class FEBasisDomain, class FEBasisTarget, class GlueType >
+typename ProjectorTraits<FEBasisDomain, FEBasisTarget>::Projector
+makeProjector(const FEBasisDomain& feBasisDomain,
+              const FEBasisTarget& feBasisTarget,
+              GlueType glue)
+{
+    // we assume that target dim <= domain dimension
+    static constexpr int domainDim = FEBasisDomain::GridView::dimension;
+    static constexpr int targetDim = FEBasisTarget::GridView::dimension;
+    static_assert(targetDim <= domainDim, "makeProjectorPair() expects targetDim < domainDim, please swap arguments");
+
+    return Impl::makeProjectorPair<false>(feBasisDomain, feBasisTarget, glue).first;
+}
+
+} // end namespace Dumux
+
+#endif // DUMUX_PROJECTOR_HH