diff --git a/CMakeLists.txt b/CMakeLists.txt
index d1187c1bfb69fb0a9a50b81dc873da948243d1f0..fe143b1f57c2de58a915fb2bfaa02c77efa01c6d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -163,6 +163,7 @@ set(${PROJECT_NAME}_HEADERS
     include/eigenpy/register.hpp
     include/eigenpy/std-map.hpp
     include/eigenpy/std-vector.hpp
+    include/eigenpy/optional.hpp
     include/eigenpy/pickle-vector.hpp
     include/eigenpy/stride.hpp
     include/eigenpy/tensor/eigen-from-python.hpp
diff --git a/include/eigenpy/optional.hpp b/include/eigenpy/optional.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8394ba7244ad43c5f548a241979338f24fafa8af
--- /dev/null
+++ b/include/eigenpy/optional.hpp
@@ -0,0 +1,144 @@
+/// Copyright (c) 2023 CNRS INRIA
+/// Definitions for exposing boost::optional<T> types.
+/// Also works with std::optional.
+
+#ifndef __eigenpy_optional_hpp__
+#define __eigenpy_optional_hpp__
+
+#include "eigenpy/fwd.hpp"
+#include "eigenpy/eigen-from-python.hpp"
+#include <boost/optional.hpp>
+#ifdef EIGENPY_WITH_CXX17_SUPPORT
+#include <optional>
+#endif
+
+#ifndef EIGENPY_DEFAULT_OPTIONAL
+#define EIGENPY_DEFAULT_OPTIONAL boost::optional
+#endif
+
+namespace boost {
+namespace python {
+namespace converter {
+
+template <typename T>
+struct expected_pytype_for_arg<boost::optional<T> >
+    : expected_pytype_for_arg<T> {};
+
+#ifdef EIGENPY_WITH_CXX17_SUPPORT
+template <typename T>
+struct expected_pytype_for_arg<std::optional<T> > : expected_pytype_for_arg<T> {
+};
+#endif
+
+}  // namespace converter
+}  // namespace python
+}  // namespace boost
+
+namespace eigenpy {
+namespace detail {
+
+/// Helper struct to decide which type is the "none" type for a specific
+/// optional<T> implementation.
+template <template <typename> class OptionalTpl>
+struct nullopt_helper {};
+
+template <>
+struct nullopt_helper<boost::optional> {
+  typedef boost::none_t type;
+  static type value() { return boost::none; }
+};
+
+#ifdef EIGENPY_WITH_CXX17_SUPPORT
+template <>
+struct nullopt_helper<std::optional> {
+  typedef std::nullopt_t type;
+  static type value() { return std::nullopt; }
+};
+#endif
+
+template <typename T,
+          template <typename> class OptionalTpl = EIGENPY_DEFAULT_OPTIONAL>
+struct OptionalToPython {
+  static PyObject *convert(const OptionalTpl<T> &obj) {
+    if (obj)
+      return bp::incref(bp::object(*obj).ptr());
+    else {
+      return bp::incref(bp::object().ptr());  // None
+    }
+  }
+
+  static PyTypeObject const *get_pytype() {
+    return bp::converter::registered_pytype<T>::get_pytype();
+  }
+
+  static void registration() {
+    bp::to_python_converter<OptionalTpl<T>, OptionalToPython, true>();
+  }
+};
+
+template <typename T,
+          template <typename> class OptionalTpl = EIGENPY_DEFAULT_OPTIONAL>
+struct OptionalFromPython {
+  static void *convertible(PyObject *obj_ptr);
+
+  static void construct(PyObject *obj_ptr,
+                        bp::converter::rvalue_from_python_stage1_data *memory);
+
+  static void registration();
+};
+
+template <typename T, template <typename> class OptionalTpl>
+void *OptionalFromPython<T, OptionalTpl>::convertible(PyObject *obj_ptr) {
+  if (obj_ptr == Py_None) {
+    return obj_ptr;
+  }
+  bp::extract<T> bp_obj(obj_ptr);
+  if (!bp_obj.check())
+    return 0;
+  else
+    return obj_ptr;
+}
+
+template <typename T, template <typename> class OptionalTpl>
+void OptionalFromPython<T, OptionalTpl>::construct(
+    PyObject *obj_ptr, bp::converter::rvalue_from_python_stage1_data *memory) {
+  // create storage
+  using rvalue_storage_t =
+      bp::converter::rvalue_from_python_storage<OptionalTpl<T> >;
+  void *storage =
+      reinterpret_cast<rvalue_storage_t *>(reinterpret_cast<void *>(memory))
+          ->storage.bytes;
+
+  if (obj_ptr == Py_None) {
+    new (storage) OptionalTpl<T>(nullopt_helper<OptionalTpl>::value());
+  } else {
+    const T value = bp::extract<T>(obj_ptr);
+    new (storage) OptionalTpl<T>(value);
+  }
+
+  memory->convertible = storage;
+}
+
+template <typename T, template <typename> class OptionalTpl>
+void OptionalFromPython<T, OptionalTpl>::registration() {
+  bp::converter::registry::push_back(
+      &convertible, &construct, bp::type_id<OptionalTpl<T> >(),
+      bp::converter::expected_pytype_for_arg<OptionalTpl<T> >::get_pytype);
+}
+
+}  // namespace detail
+
+/// Register converters for the type `optional<T>` to Python.
+/// By default \tparam optional is `EIGENPY_DEFAULT_OPTIONAL`.
+template <typename T,
+          template <typename> class OptionalTpl = EIGENPY_DEFAULT_OPTIONAL>
+struct OptionalConverter {
+  static void registration() {
+    detail::OptionalToPython<T, OptionalTpl>::registration();
+    detail::OptionalFromPython<T, OptionalTpl>::registration();
+  }
+};
+
+}  // namespace eigenpy
+
+#endif  // __eigenpy_optional_hpp__
diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt
index cccdc44ead6ac200929ad5fd9327b182875afd41..9736ded071268fcf0453927f8d55d9fc69c96945 100644
--- a/unittest/CMakeLists.txt
+++ b/unittest/CMakeLists.txt
@@ -40,6 +40,28 @@ endif()
 add_lib_unit_test(std_vector)
 add_lib_unit_test(user_struct)
 
+function(config_bind_optional tagname opttype)
+  set(MODNAME bind_optional_${tagname})
+  set(OPTIONAL ${opttype})
+  configure_file(bind_optional.cpp.in ${MODNAME}.cpp)
+
+  set(py_file test_optional_${tagname}.py)
+  configure_file(python/test_optional.py.in
+                 ${CMAKE_CURRENT_SOURCE_DIR}/python/${py_file})
+  add_lib_unit_test(${MODNAME})
+  message(
+    STATUS
+      "Adding unit test py-optional-${tagname} with file ${py_file} and module ${MODNAME}"
+  )
+  add_python_unit_test("py-optional-${tagname}" "unittest/python/${py_file}"
+                       "unittest")
+endfunction()
+
+config_bind_optional(boost "boost::optional")
+if(CMAKE_CXX_STANDARD GREATER 14 AND CMAKE_CXX_STANDARD LESS 98)
+  config_bind_optional(std "std::optional")
+endif()
+
 add_python_unit_test("py-matrix" "unittest/python/test_matrix.py" "unittest")
 
 add_python_unit_test("py-tensor" "unittest/python/test_tensor.py" "unittest")
diff --git a/unittest/bind_optional.cpp.in b/unittest/bind_optional.cpp.in
new file mode 100644
index 0000000000000000000000000000000000000000..f71773d6505fa7dd5c76174c8b9ac6db5359ab9e
--- /dev/null
+++ b/unittest/bind_optional.cpp.in
@@ -0,0 +1,79 @@
+///
+/// Copyright (c) 2023 CNRS INRIA
+///
+
+#include "eigenpy/eigenpy.hpp"
+#include "eigenpy/optional.hpp"
+#ifdef EIGENPY_WITH_CXX17_SUPPORT
+#include <optional>
+#endif
+
+#cmakedefine OPTIONAL @OPTIONAL@
+
+typedef eigenpy::detail::nullopt_helper<OPTIONAL> none_helper;
+static auto OPT_NONE = none_helper::value();
+typedef OPTIONAL<double> opt_dbl;
+
+struct mystruct {
+  OPTIONAL<int> a;
+  opt_dbl b;
+  OPTIONAL<std::string> msg{"i am struct"};
+  mystruct() : a(OPT_NONE), b(OPT_NONE) {}
+  mystruct(int a, const opt_dbl &b = OPT_NONE) : a(a), b(b) {}
+};
+
+OPTIONAL<int> none_if_zero(int i) {
+  if (i == 0)
+    return OPT_NONE;
+  else
+    return i;
+}
+
+OPTIONAL<mystruct> create_if_true(bool flag, opt_dbl b = OPT_NONE) {
+  if (flag) {
+    return mystruct(0, b);
+  } else {
+    return OPT_NONE;
+  }
+}
+
+OPTIONAL<Eigen::MatrixXd> random_mat_if_true(bool flag) {
+  if (flag)
+    return Eigen::MatrixXd(Eigen::MatrixXd::Random(4, 4));
+  else
+    return OPT_NONE;
+}
+
+BOOST_PYTHON_MODULE(@MODNAME@) {
+  using namespace eigenpy;
+  OptionalConverter<int, OPTIONAL>::registration();
+  OptionalConverter<double, OPTIONAL>::registration();
+  OptionalConverter<std::string, OPTIONAL>::registration();
+  OptionalConverter<mystruct, OPTIONAL>::registration();
+  OptionalConverter<Eigen::MatrixXd, OPTIONAL>::registration();
+  enableEigenPy();
+
+  bp::class_<mystruct>("mystruct", bp::no_init)
+      .def(bp::init<>(bp::args("self")))
+      .def(bp::init<int, bp::optional<const opt_dbl &> >(
+          bp::args("self", "a", "b")))
+      .add_property(
+          "a",
+          bp::make_getter(&mystruct::a,
+                          bp::return_value_policy<bp::return_by_value>()),
+          bp::make_setter(&mystruct::a))
+      .add_property(
+          "b",
+          bp::make_getter(&mystruct::b,
+                          bp::return_value_policy<bp::return_by_value>()),
+          bp::make_setter(&mystruct::b))
+      .add_property(
+          "msg",
+          bp::make_getter(&mystruct::msg,
+                          bp::return_value_policy<bp::return_by_value>()),
+          bp::make_setter(&mystruct::msg));
+
+  bp::def("none_if_zero", none_if_zero, bp::args("i"));
+  bp::def("create_if_true", create_if_true, bp::args("flag", "b"));
+  bp::def("random_mat_if_true", random_mat_if_true, bp::args("flag"));
+}
diff --git a/unittest/python/test_optional.py.in b/unittest/python/test_optional.py.in
new file mode 100644
index 0000000000000000000000000000000000000000..bd5e085ec076bf2a5ce3ed8645f3b3624b95b494
--- /dev/null
+++ b/unittest/python/test_optional.py.in
@@ -0,0 +1,67 @@
+import importlib
+
+bind_optional = importlib.import_module("@MODNAME@")
+
+
+def test_none_if_zero():
+    x = bind_optional.none_if_zero(0)
+    y = bind_optional.none_if_zero(-1)
+    assert x is None
+    assert y == -1
+
+
+def test_struct_ctors():
+    # test struct ctors
+
+    struct = bind_optional.mystruct()
+    assert struct.a is None
+    assert struct.b is None
+    assert struct.msg == "i am struct"
+
+    ## no 2nd arg automatic overload using bp::optional
+    struct = bind_optional.mystruct(2)
+    assert struct.a == 2
+    assert struct.b is None
+
+    struct = bind_optional.mystruct(13, -1.0)
+    assert struct.a == 13
+    assert struct.b == -1.0
+
+
+def test_struct_setters():
+    struct = bind_optional.mystruct()
+    struct.a = 1
+    assert struct.a == 1
+
+    struct.b = -3.14
+    assert struct.b == -3.14
+
+    # set to None
+    struct.a = None
+    struct.b = None
+    struct.msg = None
+    assert struct.a is None
+    assert struct.b is None
+    assert struct.msg is None
+
+
+def test_factory():
+    struct = bind_optional.create_if_true(False, None)
+    assert struct is None
+    struct = bind_optional.create_if_true(True, None)
+    assert struct.a == 0
+    assert struct.b is None
+
+
+def test_random_mat():
+    M = bind_optional.random_mat_if_true(False)
+    assert M is None
+    M = bind_optional.random_mat_if_true(True)
+    assert M.shape == (4, 4)
+
+
+test_none_if_zero()
+test_struct_ctors()
+test_struct_setters()
+test_factory()
+test_random_mat()
diff --git a/unittest/python/test_optional_boost.py b/unittest/python/test_optional_boost.py
new file mode 100644
index 0000000000000000000000000000000000000000..fc818739d90e89be9d41f8f26c0f1f560ef5f4b7
--- /dev/null
+++ b/unittest/python/test_optional_boost.py
@@ -0,0 +1,67 @@
+import importlib
+
+bind_optional = importlib.import_module("bind_optional_boost")
+
+
+def test_none_if_zero():
+    x = bind_optional.none_if_zero(0)
+    y = bind_optional.none_if_zero(-1)
+    assert x is None
+    assert y == -1
+
+
+def test_struct_ctors():
+    # test struct ctors
+
+    struct = bind_optional.mystruct()
+    assert struct.a is None
+    assert struct.b is None
+    assert struct.msg == "i am struct"
+
+    ## no 2nd arg automatic overload using bp::optional
+    struct = bind_optional.mystruct(2)
+    assert struct.a == 2
+    assert struct.b is None
+
+    struct = bind_optional.mystruct(13, -1.0)
+    assert struct.a == 13
+    assert struct.b == -1.0
+
+
+def test_struct_setters():
+    struct = bind_optional.mystruct()
+    struct.a = 1
+    assert struct.a == 1
+
+    struct.b = -3.14
+    assert struct.b == -3.14
+
+    # set to None
+    struct.a = None
+    struct.b = None
+    struct.msg = None
+    assert struct.a is None
+    assert struct.b is None
+    assert struct.msg is None
+
+
+def test_factory():
+    struct = bind_optional.create_if_true(False, None)
+    assert struct is None
+    struct = bind_optional.create_if_true(True, None)
+    assert struct.a == 0
+    assert struct.b is None
+
+
+def test_random_mat():
+    M = bind_optional.random_mat_if_true(False)
+    assert M is None
+    M = bind_optional.random_mat_if_true(True)
+    assert M.shape == (4, 4)
+
+
+test_none_if_zero()
+test_struct_ctors()
+test_struct_setters()
+test_factory()
+test_random_mat()
diff --git a/unittest/python/test_optional_std.py b/unittest/python/test_optional_std.py
new file mode 100644
index 0000000000000000000000000000000000000000..69949a44f46b3f2ea010af24a3478d0a67fb8e5f
--- /dev/null
+++ b/unittest/python/test_optional_std.py
@@ -0,0 +1,67 @@
+import importlib
+
+bind_optional = importlib.import_module("bind_optional_std")
+
+
+def test_none_if_zero():
+    x = bind_optional.none_if_zero(0)
+    y = bind_optional.none_if_zero(-1)
+    assert x is None
+    assert y == -1
+
+
+def test_struct_ctors():
+    # test struct ctors
+
+    struct = bind_optional.mystruct()
+    assert struct.a is None
+    assert struct.b is None
+    assert struct.msg == "i am struct"
+
+    ## no 2nd arg automatic overload using bp::optional
+    struct = bind_optional.mystruct(2)
+    assert struct.a == 2
+    assert struct.b is None
+
+    struct = bind_optional.mystruct(13, -1.0)
+    assert struct.a == 13
+    assert struct.b == -1.0
+
+
+def test_struct_setters():
+    struct = bind_optional.mystruct()
+    struct.a = 1
+    assert struct.a == 1
+
+    struct.b = -3.14
+    assert struct.b == -3.14
+
+    # set to None
+    struct.a = None
+    struct.b = None
+    struct.msg = None
+    assert struct.a is None
+    assert struct.b is None
+    assert struct.msg is None
+
+
+def test_factory():
+    struct = bind_optional.create_if_true(False, None)
+    assert struct is None
+    struct = bind_optional.create_if_true(True, None)
+    assert struct.a == 0
+    assert struct.b is None
+
+
+def test_random_mat():
+    M = bind_optional.random_mat_if_true(False)
+    assert M is None
+    M = bind_optional.random_mat_if_true(True)
+    assert M.shape == (4, 4)
+
+
+test_none_if_zero()
+test_struct_ctors()
+test_struct_setters()
+test_factory()
+test_random_mat()