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..38f167db75f13b7112fa904ecee787e87dad8825
--- /dev/null
+++ b/include/eigenpy/optional.hpp
@@ -0,0 +1,114 @@
+/// 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>
+
+#define EIGENPY_DEFAULT_OPTIONAL boost::optional
+
+namespace boost {
+namespace python {
+namespace converter {
+
+template <typename T>
+struct expected_pytype_for_arg<EIGENPY_DEFAULT_OPTIONAL<T> >
+    : expected_pytype_for_arg<T> {};
+
+}  // namespace converter
+}  // namespace python
+}  // namespace boost
+
+namespace eigenpy {
+namespace detail {
+
+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>(boost::none);
+  } 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..9d62420f240338d5926f7f3963234643e514393f 100644
--- a/unittest/CMakeLists.txt
+++ b/unittest/CMakeLists.txt
@@ -39,6 +39,7 @@ if(NOT NUMPY_WITH_BROKEN_UFUNC_SUPPORT)
 endif()
 add_lib_unit_test(std_vector)
 add_lib_unit_test(user_struct)
+add_lib_unit_test(bind_optional)
 
 add_python_unit_test("py-matrix" "unittest/python/test_matrix.py" "unittest")
 
@@ -97,3 +98,6 @@ set_tests_properties("py-std-vector" PROPERTIES DEPENDS ${PYWRAP})
 add_python_unit_test("py-user-struct" "unittest/python/test_user_struct.py"
                      "python;unittest")
 set_tests_properties("py-std-vector" PROPERTIES DEPENDS ${PYWRAP})
+
+add_python_unit_test("py-optional" "unittest/python/test_optional.py"
+                     "unittest")
diff --git a/unittest/bind_optional.cpp b/unittest/bind_optional.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6d85795c4306e2f435250f6deffcceb14aba9f33
--- /dev/null
+++ b/unittest/bind_optional.cpp
@@ -0,0 +1,71 @@
+#include "eigenpy/eigenpy.hpp"
+#include "eigenpy/optional.hpp"
+
+#define OPTIONAL boost::optional
+#define OPT_NONE boost::none
+
+using opt_dbl = OPTIONAL<double>;
+
+struct mystruct {
+  OPTIONAL<int> a;
+  opt_dbl b;
+  OPTIONAL<std::string> msg{"i am struct"};
+  mystruct() : a(OPT_NONE), b(boost::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(bind_optional) {
+  using namespace eigenpy;
+  OptionalConverter<int>::registration();
+  OptionalConverter<double>::registration();
+  OptionalConverter<std::string>::registration();
+  OptionalConverter<mystruct>::registration();
+  OptionalConverter<Eigen::MatrixXd>::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 b/unittest/python/test_optional.py
new file mode 100644
index 0000000000000000000000000000000000000000..d6a29738c2b4c77edfadba79b22d3a6c334fc8e3
--- /dev/null
+++ b/unittest/python/test_optional.py
@@ -0,0 +1,65 @@
+import bind_optional
+
+
+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)
+
+
+if __name__ == "__main__":
+    import pytest
+    import sys
+
+    sys.exit(pytest.main(sys.argv))