diff --git a/requirements.txt b/requirements.txt
index cb1a66eaf..cdc4294fd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,3 +13,4 @@ termcolor >= 1.1.0
 trimesh >= 2.37.22
 # Required by trimesh.
 networkx
+
diff --git a/tensorflow_graphics/projects/point_convolutions/.flake8 b/tensorflow_graphics/projects/point_convolutions/.flake8
new file mode 100644
index 000000000..f4e3292ee
--- /dev/null
+++ b/tensorflow_graphics/projects/point_convolutions/.flake8
@@ -0,0 +1,32 @@
+[flake8]
+inline-quotes = double
+max-line-length = 79 
+max-complexity = 10
+exclude = .git,
+          .tox,
+          .pytest_cache,
+          __pycache__,
+          tensorflow_graphics/projects/*
+          tensorflow_graphics/submodules/*
+ignore = C901,
+         E101,
+         E111,
+         E114,
+         E121,
+         E125,
+         E126,
+         E129,
+         E221,
+         E265,
+         E271,
+         E305,
+         E306,
+         #E501,
+         E502,
+         E731,
+         E741,
+         F401,
+         F812,
+         W191,
+         W503,
+         W504,
\ No newline at end of file
diff --git a/tensorflow_graphics/projects/point_convolutions/LICENSE b/tensorflow_graphics/projects/point_convolutions/LICENSE
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/tensorflow_graphics/projects/point_convolutions/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/tensorflow_graphics/projects/point_convolutions/README.md b/tensorflow_graphics/projects/point_convolutions/README.md
new file mode 100644
index 000000000..83db3fe66
--- /dev/null
+++ b/tensorflow_graphics/projects/point_convolutions/README.md
@@ -0,0 +1,70 @@
+# TensorFlow Graphics Point Cloud Convolutions
+
+This module contains a python TensorFlow module `pylib` and a custom ops package in `tfg_custom_ops`.
+While it is possible to run without the custom ops package, it is strongly advised to install it for performance and memory efficiency.
+
+## Content
+
+This code contains all necessary operations to perform point cloud convolutions
+
+1. Datastructure
+    - Point cloud class for batches of arbitrary sized point clouds.
+    - Memory efficient regular grid data structure
+2. Point cloud operations
+    - Neighborhood computation
+    - Point density estimation
+    - Spatial sampling
+      - Poisson Disk sampling
+      - Cell average sampling
+3. Convolution kernels 
+    - Kernel Point Convolutions
+      - linear interpolation
+      - gaussian interpolation
+      - deformable points with regularization loss as in [KPConv](https://arxiv.org/abs/1904.08889)
+    - MLP
+      - multiple MLPs as in [MCConv](https://arxiv.org/abs/1806.01759)
+      - single MLP as in [PointConv](https://arxiv.org/abs/1811.07246)
+4. Feature aggregation inside receptive fields
+    - Monte-Carlo integration with pdf
+    - Constant summation
+5. Easy to use classes for building models
+    - `PointCloud` class
+    - `PointHierarchy` for sequential downsampling of point clouds
+    - layer classes
+      - `MCConv`
+      - `PointConv`
+      - `KPConv`
+      - `Conv1x1`
+
+## Installation
+
+Precompiled versions of the custom ops package are provided in `custom_ops/pkg_builds/tf_*` for the latest TensorFlow versions.
+For compilation instructions see the [README](custom_ops/README.md) in the `custom_ops` folder.
+
+To install it run the following command (replace `VERSION` with your installed TensorFlow version, e.g. `2.3.0`)
+```bash
+  pip install custom_ops/tf_VERSION/*.whl
+```
+
+## Tutorials
+
+Check out the Colab notebooks for an introduction to the code
+
+- [Introduction](pylib/notebooks/Introduction.ipynb)
+- [Classification on ModelNet40](pylib/notebooks/ModelNet40.ipynb) 
+
+## Unit tests
+
+Unit tests can be evaluated using
+
+```bash
+  pip install -r pytest_requirements.txt
+  pytest pylib/
+```
+
+These include tests of the custom ops if they are installed.
+
+## Additional Information
+
+You may use this software under the
+[Apache 2.0 License](https://github.com/schellmi42/tensorflow_graphics_point_clouds/blob/master/LICENSE).
\ No newline at end of file
diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/__init__.py b/tensorflow_graphics/projects/point_convolutions/pylib/__init__.py
new file mode 100755
index 000000000..73eba35d2
--- /dev/null
+++ b/tensorflow_graphics/projects/point_convolutions/pylib/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2020 The TensorFlow Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific
+
+from pylib import pc
+# from pylib import io
diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/pc/PointCloud.py b/tensorflow_graphics/projects/point_convolutions/pylib/pc/PointCloud.py
new file mode 100755
index 000000000..e08cf279a
--- /dev/null
+++ b/tensorflow_graphics/projects/point_convolutions/pylib/pc/PointCloud.py
@@ -0,0 +1,281 @@
+# Copyright 2020 The TensorFlow Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Class to represent a point cloud."""
+
+import tensorflow as tf
+from tensorflow_graphics.geometry.convolution.utils import \
+    flatten_batch_to_2d, unflatten_2d_to_batch
+
+from pylib.pc.utils import check_valid_point_cloud_input
+
+
+class _AABB:
+  """Axis aligned bounding box of a point cloud.
+
+  Args:
+    Pointcloud: A 'PointCloud' instance from which to compute the
+      axis aligned bounding box.
+
+  """
+
+  def __init__(self, point_cloud, name=None):
+
+    self._batch_size = point_cloud._batch_size
+    self._batch_shape = point_cloud._batch_shape
+    self.point_cloud_ = point_cloud
+
+    self._aabb_min = tf.math.unsorted_segment_min(
+        data=point_cloud._points, segment_ids=point_cloud._batch_ids,
+        num_segments=self._batch_size) - 1e-9
+    self._aabb_max = tf.math.unsorted_segment_max(
+        data=point_cloud._points, segment_ids=point_cloud._batch_ids,
+        num_segments=self._batch_size) + 1e-9
+
+  def get_diameter(self, ord='euclidean', name=None):
+    """ Returns the diameter of the bounding box.
+
+    Note:
+      In the following, A1 to An are optional batch dimensions.
+
+    Args:
+      ord: Order of the norm. Supported values are `'euclidean'`,
+          `1`, `2`, `np.inf` and any positive real number yielding the
+          corresponding p-norm. Default is `'euclidean'`. (optional)
+    Return:
+      diam: A `float` 'Tensor' of shape `[A1, ..., An]`, diameters of the
+        bounding boxes
+
+    """
+
+    diam = tf.linalg.norm(self._aabb_max - self._aabb_min, ord=ord, axis=-1)
+    if self._batch_shape is None:
+      return diam
+    else:
+      return tf.reshape(diam, self._batch_shape)
+
+
+class PointCloud:
+  """ Class to represent point clouds.
+
+    Note:
+      In the following, A1 to An are optional batch dimensions.
+
+    Args:
+      points: A float `Tensor` either of shape `[N, D]` or of shape
+        `[A1, .., An, V, D]`, possibly padded as indicated by `sizes`.
+        Represents the point coordinates.
+      batch_ids: An `int` `Tensor` of shape `[N]` associated with the points.
+        Is required if `points` is of shape `[N, D]`.
+      sizes: An `int` `Tensor` of shape `[A1, ..., An]` indicating the
+        true input sizes in case of padding (`sizes=None` indicates no padding)
+        Note that `sizes[A1, ..., An] <= V` or `sum(sizes) == N`.
+      batch_size:  An `int`, the size of the batch.
+    """
+
+  def __init__(self,
+               points,
+               batch_ids=None,
+               batch_size=None,
+               sizes=None,
+               name=None):
+    points = tf.convert_to_tensor(value=points, dtype=tf.float32)
+    if sizes is not None:
+      sizes = tf.convert_to_tensor(value=sizes, dtype=tf.int32)
+    if batch_ids is not None:
+      batch_ids = tf.convert_to_tensor(value=batch_ids, dtype=tf.int32)
+    if batch_size is not None:
+      self._batch_size = tf.convert_to_tensor(value=batch_size, dtype=tf.int32)
+    else:
+      self._batch_size = None
+
+    check_valid_point_cloud_input(points, sizes, batch_ids)
+
+    self._sizes = sizes
+    # compatibility batch size as CPU int for graph mode
+    self._batch_size_numpy = batch_size
+    self._batch_ids = batch_ids
+    self._dimension = tf.gather(tf.shape(points), tf.rank(points) - 1)
+    self._batch_shape = None
+    self._unflatten = None
+    self._aabb = None
+
+    if points.shape.ndims > 2:
+      self._init_from_padded(points)
+    else:
+      self._init_from_segmented(points)
+
+    if self._batch_size_numpy is None:
+      self._batch_size_numpy = self._batch_size
+
+    #Sort the points based on the batch ids in incremental order.
+    self._sorted_indices_batch = tf.argsort(self._batch_ids)
+
+  def _init_from_padded(self, points):
+    """converting padded `Tensor` of shape `[A1, ..., An, V, D]` into a 2D
+      `Tensor` of shape `[N,D]` with segmentation ids.
+    """
+    self._batch_shape = tf.shape(points)[:-2]
+    if self._batch_size is None:
+      self._batch_size = tf.reduce_prod(self._batch_shape)
+    if self._sizes is None:
+      self._sizes = tf.constant(
+          value=tf.shape(points)[-2], shape=self._batch_shape)
+    self._get_segment_id = tf.reshape(
+        tf.range(0, self._batch_size), self._batch_shape)
+    self._points, self._unflatten = flatten_batch_to_2d(points, self._sizes)
+    self._batch_ids = tf.repeat(
+        tf.range(0, self._batch_size),
+        repeats=tf.reshape(self._sizes, [-1]))
+
+  def _init_from_segmented(self, points):
+    """if input is already 2D `Tensor` with segmentation ids or given sizes.
+    """
+    if self._batch_ids is None:
+      if self._batch_size is None:
+        self._batch_size = tf.reduce_prod(self._sizes.shape)
+      self._batch_ids = tf.repeat(tf.range(0, self._batch_size), self._sizes)
+    if self._batch_size is None:
+      self._batch_size = tf.reduce_max(self._batch_ids) + 1
+    self._points = points
+
+  def get_points(self, id=None, max_num_points=None, name=None):
+    """ Returns the points.
+
+    Note:
+      In the following, A1 to An are optional batch dimensions.
+
+      If called withoud specifying 'id' returns the points in padded format
+      `[A1, ..., An, V, D]`
+
+    Args:
+      id: An `int`, index of point cloud in the batch, if `None` returns all
+      max_num_points: An `int`, specifies the 'V' dimension the method returns,
+          by default uses maximum of 'sizes'. `max_rows >= max(sizes)`
+
+    Return:
+      A `float` `Tensor`
+        of shape `[Ni, D]`, if 'id' was given
+      or
+        of shape `[A1, ..., An, V, D]`, zero padded, if no `id` was given.
+
+    """
+    if id is not None:
+      if not isinstance(id, int):
+        slice = self._get_segment_id
+        for slice_id in id:
+          slice = slice[slice_id]
+        id = slice
+      if id > self._batch_size:
+        raise IndexError('batch index out of range')
+      return self._points[self._batch_ids == id]
+    else:
+      return self.get_unflatten(max_num_points=max_num_points)(self._points)
+
+  def get_sizes(self, name=None):
+    """ Returns the sizes of the point clouds in the batch.
+
+    Note:
+      In the following, A1 to An are optional batch dimensions.
+      Use this instead of accessing 'self._sizes',
+      if the class was constructed using segmented input the '_sizes' is
+      created in this method.
+
+    Returns:
+      `Tensor` of shape `[A1, .., An]`.
+
+    """
+    if self._sizes is None:
+      _ids, _, self._sizes = tf.unique_with_counts(
+          self._batch_ids)
+      _ids_sorted = tf.argsort(_ids)
+      self._sizes = tf.gather(self._sizes, _ids_sorted)
+      if self._batch_shape is not None:
+        self._sizes = tf.reshape(self._sizes, self._batch_shape)
+    return self._sizes
+
+  def get_unflatten(self, max_num_points=None, name=None):
+    """ Returns the method to unflatten the segmented points.
+
+    Use this instead of accessing 'self._unflatten',
+    if the class was constructed using segmented input the '_unflatten' method
+    is created in this method.
+
+    Note:
+      In the following, A1 to An are optional batch dimensions.
+
+    Args:
+      max_num_points: An `int`, specifies the 'V' dimension the method returns,
+        by default uses maximum of 'sizes'. `max_rows >= max(sizes)`
+    Returns:
+      A method to unflatten the segmented points, which returns a `Tensor` of
+      shape `[A1,...,An,V,D]`, zero padded.
+
+    Raises:
+      ValueError: When trying to unflatten unsorted points.
+
+    """
+    if self._unflatten is None:
+      self._unflatten = lambda data: unflatten_2d_to_batch(
+          data=tf.gather(data, self._sorted_indices_batch),
+          sizes=self.get_sizes(),
+          max_rows=max_num_points)
+    return self._unflatten
+
+  def get_AABB(self) -> _AABB:
+    """ Returns the axis aligned bounding box of the point cloud.
+
+    Use this instead of accessing `self._aabb`, as the bounding box
+    is initialized  with tthe first call of his method.
+
+    Returns:
+      A `AABB` instance
+
+    """
+    if self._aabb is None:
+      self._aabb = _AABB(point_cloud=self)
+    return self._aabb
+
+  def set_batch_shape(self, batch_shape, name=None):
+    """ Function to change the batch shape
+
+      Use this to set a batch shape instead of using 'self._batch_shape' to
+      also change dependent variables.
+
+    Note:
+      In the following, A1 to An are optional batch dimensions.
+
+    Args:
+      batch_shape: A 1D `int` `Tensor` `[A1,...,An]`.
+
+    Raises:
+      ValueError: if shape does not sum up to batch size.
+
+    """
+    if batch_shape is not None:
+      batch_shape = tf.convert_to_tensor(value=batch_shape, dtype=tf.int32)
+      tf.assert_equal(
+          tf.reduce_prod(batch_shape), self._batch_size,
+          f'Incompatible batch size. Must be {self._batch_size} \
+              but is {tf.reduce_prod(batch_shape)}')
+      # if tf.reduce_prod(batch_shape) != self._batch_size:
+      #   raise ValueError(
+      #       f'Incompatible batch size. Must be {self._batch_size} \
+      #        but is {tf.reduce_prod(batch_shape)}')
+      self._batch_shape = batch_shape
+      self._get_segment_id = tf.reshape(
+          tf.range(0, self._batch_size), self._batch_shape)
+      if self._sizes is not None:
+        self._sizes = tf.reshape(self._sizes, self._batch_shape)
+    else:
+      self._batch_shape = None
diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/pc/__init__.py b/tensorflow_graphics/projects/point_convolutions/pylib/pc/__init__.py
new file mode 100755
index 000000000..26a7badc3
--- /dev/null
+++ b/tensorflow_graphics/projects/point_convolutions/pylib/pc/__init__.py
@@ -0,0 +1,28 @@
+# Copyright 2020 The TensorFlow Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Point cloud module."""
+
+from .PointCloud import _AABB as AABB
+from .PointCloud import PointCloud
+'''
+from .Grid import Grid
+from .Neighborhood import Neighborhood
+from .Neighborhood import KDEMode
+from .sampling import poisson_disk_sampling, cell_average_sampling
+from .sampling import sample
+from .PointHierarchy import PointHierarchy
+
+from pylib.pc import layers
+from pylib.pc import custom_ops
+'''
\ No newline at end of file
diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/__init__.py b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/__init__.py
new file mode 100644
index 000000000..26540aa8e
--- /dev/null
+++ b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/__init__.py
@@ -0,0 +1,12 @@
+# Copyright 2020 The TensorFlow Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific
diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/aabb_test.py b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/aabb_test.py
new file mode 100644
index 000000000..c7635badd
--- /dev/null
+++ b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/aabb_test.py
@@ -0,0 +1,72 @@
+# Copyright 2020 The TensorFlow Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Class to test bounding box"""
+
+import os
+import sys
+import numpy as np
+import tensorflow as tf
+from absl.testing import parameterized
+from tensorflow_graphics.util import test_case
+
+from pylib.pc import PointCloud, AABB
+from pylib.pc.tests import utils
+
+
+class AABB_test(test_case.TestCase):
+
+  @parameterized.parameters(
+      (1, 1000, 3),
+      (8, 1000, 2),
+      (32, 1000, 4)
+  )
+  def test_aabb_min_max(self, batch_size, num_points, dimension):
+    points, batch_ids = utils._create_random_point_cloud_segmented(
+        batch_size, num_points, dimension)
+    aabb_max_numpy = np.empty([batch_size, dimension])
+    aabb_min_numpy = np.empty([batch_size, dimension])
+    for i in range(batch_size):
+      aabb_max_numpy[i] = np.amax(points[batch_ids == i], axis=0)
+      aabb_min_numpy[i] = np.amin(points[batch_ids == i], axis=0)
+
+    aabb_tf = PointCloud(points, batch_ids=batch_ids,
+                         batch_size=batch_size).get_AABB()
+
+    self.assertAllClose(aabb_max_numpy, aabb_tf._aabb_max)
+    self.assertAllClose(aabb_min_numpy, aabb_tf._aabb_min)
+
+  @parameterized.parameters(
+      ([1], 1000, 3),
+      ([4, 4], 1000, 2),
+      ([1, 2, 3], 100, 4)
+  )
+  def test_aabb_diameter(self, batch_shape, max_num_points, dimension):
+    points, sizes = utils._create_random_point_cloud_padded(
+        max_num_points, batch_shape, dimension)
+    batch_size = np.prod(batch_shape)
+    diameter_numpy = np.empty(batch_size)
+    points_flat = np.reshape(points, [batch_size, max_num_points, dimension])
+    sizes_flat = np.reshape(sizes, [batch_size])
+    for i in range(batch_size):
+      curr_pts = points_flat[i][:sizes_flat[i]]
+      diag = np.amax(curr_pts, axis=0) - np.amin(curr_pts, axis=0)
+      diameter_numpy[i] = np.linalg.norm(diag)
+    diameter_numpy = np.reshape(diameter_numpy, batch_shape)
+
+    aabb_tf = PointCloud(points, sizes=sizes).get_AABB()
+    diameter_tf = aabb_tf.get_diameter()
+    self.assertAllClose(diameter_numpy, diameter_tf)
+
+if __name__ == '__main__':
+  test_case.main()
diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/point_cloud_test.py b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/point_cloud_test.py
new file mode 100644
index 000000000..e71d27ff3
--- /dev/null
+++ b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/point_cloud_test.py
@@ -0,0 +1,109 @@
+# Copyright 2020 The TensorFlow Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific
+"""Class to test point clouds"""
+
+import os
+import sys
+import numpy as np
+import tensorflow as tf
+from absl.testing import parameterized
+from tensorflow_graphics.util import test_case
+
+from pylib.pc import PointCloud
+from pylib.pc.tests import utils
+
+
+class PointCloudTest(test_case.TestCase):
+
+  @parameterized.parameters(
+    ([32], 100, 3),
+    ([5, 2], 100, 2),
+    ([2, 3, 4], 100, 4)
+  )
+  def test_flatten_unflatten_padded(self, batch_shape, num_points, dimension):
+    batch_size = np.prod(batch_shape)
+    points, sizes = utils._create_random_point_cloud_padded(
+        num_points, batch_shape, dimension=dimension)
+    point_cloud = PointCloud(points, sizes=sizes)
+    retrieved_points = point_cloud.get_points().numpy()
+    self.assertAllEqual(points.shape, retrieved_points.shape)
+    points = points.reshape([batch_size, num_points, dimension])
+    retrieved_points = retrieved_points.reshape(
+        [batch_size, num_points, dimension])
+    sizes = sizes.reshape([batch_size])
+    for i in range(batch_size):
+      self.assertAllClose(points[i, :sizes[i]], retrieved_points[i, :sizes[i]])
+      self.assertTrue(np.all(retrieved_points[i, sizes[i]:] == 0))
+
+  @parameterized.parameters(
+    (100, 32, [8, 4]),
+    (100, 16, [2, 2, 2, 2])
+  )
+  def test_construction_methods(self, max_num_points, batch_size, batch_shape):
+    points, sizes = utils._create_random_point_cloud_padded(
+        max_num_points, batch_shape)
+    num_points = np.sum(sizes)
+
+    sizes_flat = sizes.reshape([batch_size])
+    points_flat = points.reshape([batch_size, max_num_points, 3])
+    batch_ids = np.repeat(np.arange(0, batch_size), sizes_flat)
+
+    points_seg = np.empty([num_points, 3])
+    cur_id = 0
+    for pts, size in zip(points_flat, sizes_flat):
+      points_seg[cur_id:cur_id + size] = pts[:size]
+      cur_id += size
+
+    pc_from_padded = PointCloud(points, sizes=sizes)
+    self.assertAllEqual(batch_ids, pc_from_padded._batch_ids)
+    self.assertAllClose(points_seg, pc_from_padded._points)
+
+    pc_from_ids = PointCloud(points_seg, batch_ids)
+    pc_from_ids.set_batch_shape(batch_shape)
+
+    pc_from_sizes = PointCloud(points_seg, sizes=sizes_flat)
+    pc_from_sizes.set_batch_shape(batch_shape)
+    self.assertAllEqual(batch_ids, pc_from_sizes._batch_ids)
+
+    points_from_padded = pc_from_padded.get_points(
+        max_num_points=max_num_points)
+    points_from_ids = pc_from_ids.get_points(
+        max_num_points=max_num_points)
+    points_from_sizes = pc_from_sizes.get_points(
+        max_num_points=max_num_points)
+
+    self.assertAllEqual(points_from_padded, points_from_ids)
+    self.assertAllEqual(points_from_ids, points_from_sizes)
+    self.assertAllEqual(points_from_sizes, points_from_padded)
+
+  @parameterized.parameters(
+    (1000,
+     ['Invalid input! Point tensor is of dimension 1 \
+        but should be at least 2!',
+      'Missing input! Either sizes or batch_ids must be given.',
+      'Invalid sizes! Sizes of points and batch_ids are not equal.'])
+  )
+  def test_exceptions_raised_at_construction(self, num_points, msgs):
+    points = np.random.rand(num_points)
+    batch_ids = np.zeros(num_points)
+    with self.assertRaisesRegex(ValueError, msgs[0]):
+      _ = PointCloud(points, batch_ids)
+    points = np.random.rand(num_points, 3)
+    with self.assertRaisesRegexp(ValueError, msgs[1]):
+      _ = PointCloud(points)
+    with self.assertRaisesRegexp(AssertionError, msgs[2]):
+      _ = PointCloud(points, batch_ids[1:])
+
+
+if __name__ == '__main__':
+  test_case.main()
diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/utils.py b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/utils.py
new file mode 100644
index 000000000..e7ec1e1e9
--- /dev/null
+++ b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/utils.py
@@ -0,0 +1,82 @@
+# Copyright 2020 The TensorFlow Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""helper functions for unit tests"""
+
+import tensorflow as tf
+import numpy as np
+
+
+def _create_random_point_cloud_segmented(batch_size,
+                                         num_points,
+                                         dimension=3,
+                                         sizes=None,
+                                         scale=1,
+                                         clean_aabb=False,
+                                         equal_sized_batches=False):
+  points = np.random.uniform(0, scale, [num_points, dimension])
+  if sizes is None:
+    if not equal_sized_batches:
+      batch_ids = np.random.randint(0, batch_size, num_points)
+      batch_ids[:batch_size] = np.arange(0, batch_size)
+    else:
+      batch_ids = np.repeat(np.arange(0, batch_size), num_points // batch_size)
+    # batch_ids = np.sort(batch_ids)
+  else:
+    sizes = np.array(sizes, dtype=int)
+    batch_ids = np.repeat(np.arange(0, batch_size), sizes)
+  if clean_aabb:
+    # adds points such that the aabb is [0,0,0] [1,1,1]*scale
+    # to prevent rounding errors
+    points = np.concatenate(
+        (points, scale * np.ones([batch_size, dimension]) - 1e-9,
+         1e-9 + np.zeros([batch_size, dimension])))
+    batch_ids = np.concatenate(
+        (batch_ids, np.arange(0, batch_size), np.arange(0, batch_size)))
+  return points, batch_ids
+
+
+def _create_random_point_cloud_padded(max_num_points,
+                                      batch_shape,
+                                      dimension=3,
+                                      sizes=None,
+                                      scale=1):
+  batch_size = np.prod(batch_shape)
+  points = np.random.uniform(
+      0, scale, [max_num_points * batch_size, dimension])
+  points = points.reshape(batch_shape + [max_num_points, dimension])
+  if sizes is None:
+    sizes = np.random.randint(1, max_num_points, batch_shape)
+  return points, sizes
+
+'''
+def _create_uniform_distributed_point_cloud_2D(num_points_sqrt,
+                                               scale=1,
+                                               flat=False):
+  ticks = np.linspace(0, scale, num=num_points_sqrt)
+  points = np.array(np.meshgrid(ticks, ticks)).T
+  if flat:
+    points = points.reshape(-1, 2)
+  return points
+
+
+def _create_uniform_distributed_point_cloud_3D(num_points_root,
+                                               bb_min=0,
+                                               bb_max=1,
+                                               flat=False):
+  ticks = np.linspace(bb_min, bb_max, num=num_points_root, endpoint=False)
+  points = np.array(np.meshgrid(ticks, ticks, ticks)).T
+  if flat:
+    points = points.reshape(-1, 3)
+  return points
+'''
\ No newline at end of file
diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/pc/utils.py b/tensorflow_graphics/projects/point_convolutions/pylib/pc/utils.py
new file mode 100644
index 000000000..972222310
--- /dev/null
+++ b/tensorflow_graphics/projects/point_convolutions/pylib/pc/utils.py
@@ -0,0 +1,115 @@
+# Copyright 2020 The TensorFlow Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+''' helper functions for point clouds '''
+
+import tensorflow as tf
+from tensorflow_graphics.geometry.convolution.utils import flatten_batch_to_2d
+from pylib.pc import PointCloud
+
+
+def check_valid_point_cloud_input(points, sizes, batch_ids):
+  """Checks that the inputs to the constructor of class 'PointCloud' are valid.
+
+  Args:
+    points: A `float` `Tensor` of shape `[N, D]` or `[A1, ..., An, V, D]`.
+    sizes:  An `int` `Tensor` of shape `[A1, ..., An]` or `None`.
+    batch_ids: An `int` `Tensor` of shape `[N]` or `None`.
+
+  Raises:
+    Value Error: If input dimensions are invalid or no valid segmentation
+      is given.
+
+  """
+
+  if points.shape.ndims == 2 and sizes is None and batch_ids is None:
+    raise ValueError('Missing input! Either sizes or batch_ids must be given.')
+  if points.shape.ndims == 1:
+    raise ValueError(
+        'Invalid input! Point tensor is of dimension 1 \
+        but should be at least 2!')
+  if points.shape.ndims == 2 and batch_ids is not None:
+    if points.shape[0] != batch_ids.shape[0]:
+      raise AssertionError('Invalid sizes! Sizes of points and batch_ids are' +
+                           ' not equal.')
+
+'''
+def check_valid_point_hierarchy_input(point_cloud, cell_sizes, pool_mode):
+  """ Checks that inputs to the constructor of class 'PontHierarchy' are valid.
+
+  Args:
+    point_cloud: A 'PointCloud' instance.
+    cell_sizes: A `list` of `float` `Tensors`.
+    pool_mode: An `int`.
+
+  Raises:
+    TypeError: if input is of invalid type
+    ValueError: if pool_mode is invalid, or cell_sizes dimension are invalid
+      or non-positive
+
+  """
+  if not isinstance(point_cloud, (PointCloud)):
+    raise TypeError('Input must be instance of class PointCloud')
+  if pool_mode not in [0, 1]:
+    raise ValueError('Unknown pooling mode.')
+  for curr_cell_sizes in cell_sizes:
+    if any(curr_cell_sizes <= 0):
+      raise ValueError('cell size must be positive.')
+    if not curr_cell_sizes.shape[0] in [1, point_cloud.dimension_]:
+      raise ValueError(
+          'Invalid number of cell sizes for point cloud' +\
+          f'dimension. Must be 1 or {point_cloud.dimension_} but is' +\
+          f'{curr_cell_sizes.shape[0]}.')
+
+
+def _flatten_features(features, point_cloud: PointCloud):
+  """ Converts features of shape `[A1, ..., An, C]` to shape `[N, C]`.
+
+  Args:
+    features: A `Tensor`.
+    point_cloud: A `PointCloud` instance.
+
+  Returns:
+    A `Tensor` of shape `[N, C]`.
+
+  """
+  if features.shape.ndims > 2:
+    sizes = point_cloud.get_sizes()
+    features, _ = flatten_batch_to_2d(features, sizes)
+    sorting = tf.math.invert_permutation(point_cloud._sorted_indices_batch)
+    features = tf.gather(features, sorting)
+  else:
+    tf.assert_equal(tf.shape(features)[0], tf.shape(point_cloud._points)[0])
+  tf.assert_equal(tf.rank(features), 2)
+  return features
+
+
+def cast_to_num_dims(values, num_dims, dtype=tf.float32):
+  """ Converts an input to the specified `dtype` and repeats it `num_dims`
+  times.
+
+  Args:
+    values: Must be convertible to a `Tensor` of shape `[], [1]` or
+      `[num_dims]`.
+    dtype: A `tf.dtype`.
+
+  Returns:
+    A `dtype` `Tensor` of shape `[num_dims]`.
+
+  """
+  values = tf.cast(tf.convert_to_tensor(value=values),
+                   dtype=dtype)
+  if values.shape == [] or values.shape[0] == 1:
+    values = tf.repeat(values, num_dims)
+  return values
+'''
diff --git a/tensorflow_graphics/projects/point_convolutions/pytest.ini b/tensorflow_graphics/projects/point_convolutions/pytest.ini
new file mode 100644
index 000000000..82972425f
--- /dev/null
+++ b/tensorflow_graphics/projects/point_convolutions/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+norecursedirs = tensorflow_graphics/rendering/opengl/tests tensorflow_graphics/submodules
+python_files = *_test.py
diff --git a/tensorflow_graphics/projects/point_convolutions/pytest_requirements.txt b/tensorflow_graphics/projects/point_convolutions/pytest_requirements.txt
new file mode 100644
index 000000000..418e8a444
--- /dev/null
+++ b/tensorflow_graphics/projects/point_convolutions/pytest_requirements.txt
@@ -0,0 +1,3 @@
+pytest
+sklearn
+trimesh
\ No newline at end of file
