diff --git a/SimoxUtility/CMakeLists.txt b/SimoxUtility/CMakeLists.txt
index b39459da876abe4caebf4d4c65af2b4b22101683..dd2a7c433bd57f01791724dba4458e9fb94f9e03 100644
--- a/SimoxUtility/CMakeLists.txt
+++ b/SimoxUtility/CMakeLists.txt
@@ -40,6 +40,8 @@ SET(SOURCES
     math/pose/orthogonalize.cpp
     math/pose/interpolate.cpp
 
+    math/regression/linear3d.cpp
+
     math/statistics/Histogram1D.cpp
 
     meta/type_name.cpp
@@ -187,6 +189,8 @@ SET(INCLUDES
     math/pose/transform.h
     math/pose/interpolate.h
 
+    math/regression/linear3d.h
+
     math/similarity/cosine_similarity.h
     math/similarity/angular_similarity.h
 
diff --git a/SimoxUtility/math.h b/SimoxUtility/math.h
index 831dc4ca47cab6ec3d30ed744ca82479c25e5b6a..b57cef0caa0dce7c2474a110f47e0d200eec07a6 100644
--- a/SimoxUtility/math.h
+++ b/SimoxUtility/math.h
@@ -14,6 +14,7 @@
 #include "math/periodic_clamp.h"
 #include "math/pose.h"
 #include "math/project_to_plane.h"
+#include "math/regression.h"
 #include "math/rescale.h"
 #include "math/scale_value.h"
 #include "math/similarity.h"
diff --git a/SimoxUtility/math/regression.h b/SimoxUtility/math/regression.h
new file mode 100644
index 0000000000000000000000000000000000000000..1e6131d854a53b1695f052cb1de44b7f91765313
--- /dev/null
+++ b/SimoxUtility/math/regression.h
@@ -0,0 +1,5 @@
+#pragma once
+
+// This file is generated!
+
+#include "regression/linear3d.h"
diff --git a/SimoxUtility/math/regression/linear3d.cpp b/SimoxUtility/math/regression/linear3d.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f1a37111185b3752a549b1f9f29c770f8c0e1e46
--- /dev/null
+++ b/SimoxUtility/math/regression/linear3d.cpp
@@ -0,0 +1,69 @@
+#include "linear3d.h"
+
+#include <Eigen/Dense>
+
+
+namespace simox::math
+{
+
+    LinearRegression3D
+    LinearRegression3D::Fit(const std::vector<double>& xs, const std::vector<Eigen::Vector3d>& ys)
+    {
+        Eigen::Matrix3Xd ysMatrix(3, ys.size());
+        for (long col = 0; col < ysMatrix.cols(); ++col)
+        {
+            ysMatrix.col(col) = ys.at(col);
+        }
+
+        // The matrix of the predictor functions evaluated at the corresponding xs.
+        // Since this is a linear regression, the constant function a(t) = 1 and identity
+        // b(t) = t are used.
+        Eigen::MatrixX2d linFuncMatrix(xs.size(), 2);
+        linFuncMatrix.col(0) = Eigen::RowVectorXd::Ones(xs.size());
+        linFuncMatrix.col(1) = Eigen::Map<const Eigen::VectorXd>(xs.data(), xs.size());
+
+        // `linFuncMatrix` is poorly conditioned for xs that are close together
+        // (e.g. time stamps), so the normal equation would loose a lot of precision.
+        auto qrDecomp = linFuncMatrix.colPivHouseholderQr();
+
+        // Each coordinate can be treated individually (general multivariate regression).
+        // `coeffs` contains a_i and b_i in a_0 + b_0 * t = x, a_1 + b_1 * t = y, etc.
+        Eigen::Matrix<double, 3, 2> coeffs;
+        for (int dim = 0; dim < 3; ++dim)
+        {
+            Eigen::VectorXd coords = ysMatrix.row(dim).transpose();
+            coeffs.row(dim) = qrDecomp.solve(coords);
+        }
+
+        return LinearRegression3D { .coefficients = coeffs };
+    }
+
+
+    Eigen::Vector3d
+    LinearRegression3D::predict(double x) const
+    {
+        Eigen::Vector2d input;
+        input << 1.0, x;
+        return coefficients * input;
+    }
+
+}
+
+
+std::ostream&
+simox::math::operator<<(std::ostream& os, const LinearRegression3D& r)
+{
+    os << "<LinearRegression3D a + b * x = y [ ";
+    for (Eigen::Index row = 0; row < r.coefficients.rows(); ++row)
+    {
+        if (row != 0)
+        {
+            os << ", ";
+        }
+        os << r.coefficients(row, 0)
+           << " + " << r.coefficients(row, 1) << " * x_" << row
+           << " = y_" << row;
+    }
+    os << " ]";
+    return os;
+}
diff --git a/SimoxUtility/math/regression/linear3d.h b/SimoxUtility/math/regression/linear3d.h
new file mode 100644
index 0000000000000000000000000000000000000000..646b7ea56807c9809385b38a4ac354a048ff81ae
--- /dev/null
+++ b/SimoxUtility/math/regression/linear3d.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include <Eigen/Core>
+
+
+namespace simox::math
+{
+
+    /**
+     * @brief A linear regression model of the form a + b * x = y,
+     * or per dimension, a_i + b_i * x_i = y_i.
+     *
+     * - x is the scalar input variable (e.g. time)
+     * - y is 3D vector output variable (e.g. position)
+     * - a is a 3D bias vector
+     * - b is a 3D coefficient vector
+     *
+     * In matrix notation, the underlying equation system is:
+     *
+     * [[ a_0  b_0 ]    [  1    1    1  ]   [ y_0 ]
+     *  [ a_1  b_1 ]  * [ x_0  x_1  x_2 ] = [ y_1 ]
+     *  [ a_2  b_2 ]]                       [ y_2 ]
+     */
+    class LinearRegression3D
+    {
+    public:
+
+        /**
+         * The coefficients of the bias term and input variable x
+         * [[ a_0  b_0 ]
+         *  [ a_1  b_1 ]
+         *  [ a_2  b_2 ]]
+         */
+        Eigen::Matrix<double, 3, 2> coefficients;
+
+
+        /**
+         * @brief Fit a linear regression model to the given data.
+         * @param xs The input variables.
+         * @param ys The output variables.
+         * @return The regression model.
+         */
+        static LinearRegression3D
+        Fit(const std::vector<double>& xs, const std::vector<Eigen::Vector3d>& ys);
+
+        /**
+         * @brief Predict the output variable of the given input variable.
+         * @param x The input variable.
+         * @return The predicted output variable.
+         */
+        Eigen::Vector3d predict(double x) const;
+
+    };
+
+
+    std::ostream& operator<<(std::ostream& os, const LinearRegression3D& r);
+
+}